---
title: 'Build a B2B Application with Postgres and NextJS'
sidebarTitle: 'NextJS'
---

In this quick tutorial, you will learn about Nile's tenant virtualization features, while building a todo list application with NextJS 13.

<iframe
  width="480"
  height="270"
  src="https://www.youtube.com/embed/Eo0dDROnJGg?si=qS4Rg8LAyIkK4zfB"
  title="YouTube video player"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  allowFullScreen
></iframe>

<Steps>
  <Step title="Create a database">
    1. Go to the [Nile console](https://console.thenile.dev) and create a workspace and database if you have not done so already.
    2. You should see a welcome message. Click on "Lets get started"
       ![Nile welcome.](/images/nile-welcome.png)
    3. Give your workspace and database names, or you can accept the default auto-generated names.
  </Step>

  <Step title="Create a table">
    After you created a database, you will land in Nile's query editor. Since our application requires a table for storing all the "todos" this is a good time to create one:

    ```sql
    create table todos (
        id uuid DEFAULT (gen_random_uuid()),
        tenant_id uuid,
        title varchar(256),
        estimate varchar(256),
        complete boolean,
        embedding vector(768)
    );
    ```

    You will see the new table in the panel on the left side of the screen, and you can expand it to view the columns.

    See the `tenant_id` column? By specifying this column, You are making the table **tenant aware**. The rows in it will belong to specific tenants. If you leave it out, the table is considered shared, more on this later.
    ![Creating a table in Nile's admin dashboard](/images/gui-create-table.png)

    The embedding column is a vector representation of the task. When the user adds new tasks, we will use these embeddings to find semantically
    related tasks and use this as a basis of our AI-driven time estimates. This technique - looking up related data using embeddings and using this data with text generation models is called [**RAG (Retrieval Augmented Generation)**](https://www.thenile.dev/docs/ai-embeddings/rag).

  </Step>

{" "}

<Step title="Get credentials">
  In the left-hand menu, click on "Settings" and then select "Credentials".
  Generate credentials and keep them somewhere safe. These give you access to
  the database.
</Step>

{" "}

<Step title="Get third party credentials">
  This example uses AI chat and embedding models to generate automated time
  estimates for each task in the todo list. In order to use this functionality,
  you will need access to models from a vendor with OpenAI compatible APIs. Make
  sure you have an API key, API base URL and the [names of the models you'll
  want to use](https://www.thenile.dev/docs/ai-embeddings/embedding_models).
</Step>

  <Step title="Set the environment">
    Enough GUI for now. Let's get to some code.

    If you haven't cloned this repository yet, now will be an excellent time to do so.

    ```bash
    git clone https://github.com/niledatabase/niledatabase
    cd niledatabase/examples/quickstart/nextjs
    ```

    Rename `.env.local.example` to `.env.local`, and update it with your database credentials.

    It should look something like this (you can see that I used Fireworks as the vendor, but you can use OpenAI or any compatible vendor):

    ```bash
    # Private env vars that should never show up in the browser
    # These are used by the server to connect to Nile database
    NILEDB_USER=018ad484-0d52-7274-8639-057814be60c3
    NILEDB_PASSWORD=0d11b8e5-fbbc-4639-be44-8ab72947ec5b

    AI_API_KEY=your-ai-vendor-api-key
    AI_BASE_URL=https://api.fireworks.ai/inference/v1
    AI_MODEL=accounts/fireworks/models/llama-v3p1-405b-instruct
    EMBEDDING_MODEL=nomic-ai/nomic-embed-text-v1.5
    ```

    Install dependencies

    ```bash
    npm install
    ```

  </Step>

  <Step title="Run the application">
    ```bash
    npm run dev
    ```

    💡 Note: This example only works with Node 18 and above. You can check the version with `node -v`.

    Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

    If all went well, your browser should show you the first page in the app, asking you to login or sign up.

  </Step>

  <Step title="Check the data in Nile">
    After you sign up as a user of this example app, you'll be able to see this user by going back to Nile Console and looking at the users table

    ```sql
    select * from users;
    ```

    Login with the new user, and you can create a new tenant and add tasks for the tenant. You can see the changes in your Nile database by running

    ```sql
    select tenants.name, title, estimate, complete from
    tenants join todos on tenants.id=todos.tenant_id;
    ```

  </Step>

  <Step title="What's next?">
    This example is a good starting point for building your own application with Nile.

    You have learned basic Nile concepts and how to use them with NextJS.

    You can learn more about Nile's tenant virtualization features in the following tutorials:

    - [Tenant management](/tenant-virtualization/tenant-management)
    - [Tenant isolation](/tenant-virtualization/tenant-isolation)

    You can explore Nile's JS SDK in the [SDK reference](/auth/sdk-reference/javascript/overview).

    You can learn [More about AI in Nile](https://www.thenile.dev/docs/ai-embeddings), or try a more advanced example like:

    - [Chat with PDFs](https://www.thenile.dev/docs/getting-started/examples/chat_with_pdf)
    - [Code Assistant](https://www.thenile.dev/docs/getting-started/examples/code_assistant)

  </Step>
</Steps>

## How does it work?

There are a few moving pieces here, so let's break it down.

This example uses NextJS `app router`, so the application landing page is in `app/page.tsx`.
We'll start here and go over the code for creating tenants, selecting tenants and adding tasks.

The SDK uses your credentials to call the Nile API. Every page, route and function in our app can use the same `nile` instance to access Nile APIs and DB.

But, we need to make sure we are using the right user and tenant context.
So we call `configureNile`, which take the cookies from `next/headers` and the tenant ID.
After this point, we can use `nile` to access the database and APIs, and it will use the authenticated user and tenant context.

In addition to accessing the database and requesting data server side, there are also APIs that are integrated for you via [dynamic routes](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes).
The route file `api/[...nile]/route.ts` intercepts routes from the client and responds accordingly. There is a single file that is used to export the nile instance into the routes, which is then used to export the various handlers.

In addition, the NextJS extension automatically handles obtaining the cookies from the request.

```typescript
import { Nile } from '@niledatabase/server';
import { nextJs } from '@niledatabase/nextjs';
export const nile = Nile({ extensions: [nextJs] });
```

### Listing tenants

Next thing we do in `app/tenants/page.tsx` is to list all the tenants for the current user, but the SDK must know about the user. The simplest way to do that is to pass the NextJS cookies to the function.
We use Nile SDK to query the database for all the tenants for the current user.

```typescript
const tenants = await ctx.tenants.list();
```

and then we render the list of tenants as a list of links:

```typescript
<div className="p-6 border rounded-lg flex flex-col gap-2">
  {tenants.map((tenant: any) => (
    <NextLink href={`/tenants/${tenant.id}/todos`} key={tenant.id}>
      <Button>{tenant.name}</Button>
    </NextLink>
  ))}
</div>
```

It looks a bit like magic, but [NextJS server fetching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries) and Nile SDK are integrated together to make this happen.

### Creating a tenant

When you click on the "Create Tenant" button, we pop out a modal form. The form is defined in `app/tenants/add-form.tsx` and uses [NextJS server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations).
Create tenant logic is in `app/tenants/tenant-actions.tsx`.

In `createTenant` method, we use Nile SDK to create a new tenant and link it to the current user.

```typescript
const createTenantResponse = await nile.tenants.create({
  name: tenantName,
});
```

This call inserts a new tenant to `tenants` table, and also inserts a new row to `tenant_users` table, linking the current user to the new tenant.
You can see the new tenant in the Nile console by running `select * from tenants`.

Because the user is authenticated to the API, Nile automatically adds the user to the tenant they are creating.

### Listing tasks

When you create a tenant or select an existing tenant, you are taken to the tenant's todo list.
The code for this page is in `app/tenants/[tenantid]/todos/page.tsx`.

Since there is no API for listing specific data in your database, it is now time to being writing queries. Normally, you would write a `where` clause to accomplish some tenant-level isolation (and you still can), the Nile SDK does it a little bit differently.
Instead of using SQL `where` clauses to get only todos for this specific tenant, we are relying on Nile's tenant isolation feature:

```typescript
const _nile = await nile.withContext({ tenantId });
// no need for where clause because we previously set Nile context
const todos = await _nile.query('select * from todos');
```

Note that this time we are configuring Nile with the tenant ID too. Previously called `configureNile` without parameters because there was no tenant yet.

Behind the scenes, Nile does two things:

- Check that the user is a member of the tenant and indeed has access to these todos
- Apply tenant isolation and execute the query in a "virtual tenant DB".

This also means that you don't need to implement the 3-way join between `todo`, `tenants` and `tenant_users`, so the code is simpler and more performant.

### Adding a task

When you click on the "+" to add a new task or on the checkbox to mark it as done, we again rely on NextJS server actions to make things simple.
You can find the logic for adding a task in `app/tenants/[tenantid]/todos/todo-actions.tsx`.

As you can see, we again use the tenant context to insert the new task:

```typescript
const _nile = await nile.withContext({ tenantId });
await _nile.query(
  `INSERT INTO todos (tenant_id, title, estimate, embedding, complete)
    VALUES ($1, $2, $3, $4, $5)`,
  [
    tenantNile.tenantId,
    title,
    estimate?.substring(0, 255),
    embeddingToSQL(embedding),
    false,
  ],
);
```

or update an existing one:

```typescript
const _nile = await nile.withContext({ tenantId });
await _nile.query(
  `UPDATE todos 
  SET complete = $1
  WHERE id = $2`,
  [complete, id],
);
```

By setting the context, Nile will validate that the user indeed has permission to add tasks for this tenant,
and will make sure we only update tasks for the current tenant.

We don't want just anyone handing us tasks and telling us to do them, right?

### AI-driven time estimates

This example uses AI chat and embedding models to generate automated time estimates for each task in the todo list. We handle the time estimates in
`app/tenants/[tenantid]/todos/todo-actions.tsx`, when we add a new task.

When you add a new task, we use the embedding model to generate an embedding for the task text:

```typescript
const embedding = await embedTask(title.toString());
```

The `embedTask` function is defined in `lib/AiUtils.tsx` and uses the embedding model to generate the embedding.
We wrap the call to the model since some vendors have slightly different inputs and outputs. This will let us switch vendors easily in the future.

```typescript
// generate embeddings
let resp = await ai.embeddings.create({
  model: embedding_model,
  input: title,
});
```

Then we use the AI model to generate a time estimate for the task. We also wrapped this in a utility function, so the `addTodo` handler calls it:

```typescript
const estimate = await aiEstimate(tenantNile, title.toString());
```

The `aiEstimate` function is defined in `lib/AiUtils.tsx` and first it looks up similar tasks using embeddings. Because we are still in the tenant
context (see the use of `tenantNile` client), we only look up tasks for this tenant:

```typescript
const similarTasks = await tenantNile.query(
  `SELECT title, estimate FROM todos WHERE embedding <-> $1 < 1`,
  [embeddingToSQL(embedding)],
);
```

and finally, we use the AI model to generate the estimate. We include the similar tasks in the prompt to the model, so it can use them as reference:

```typescript
const aiEstimate = await ai.chat.completions.create({
  messages: [
    {
      role: 'user',
      content: `you are an amazing project manager. I need to ${title}. How long do you think this will take?
        I have a few similar tasks with their estimates, please use them as reference: ${similarTasks}.
        respond with just the estimate, no yapping.`,
    },
  ],
  model: model,
});
```

This estimate is then stored in the database along with the task and its vector embedding.
